MongoDB学习(五)一一MongoDB 原子操作与自动增长

MongoDB ObjectId

ObjectId介绍

在前面的学习中中我们已经使用了MongoDB 的对象 Id(ObjectId)。

接下来,我们将了解的ObjectId的结构。

ObjectId 是一个12字节 BSON 类型数据,有以下格式:

  • 前4个字节表示时间戳

  • 接下来的3个字节是机器标识码

  • 紧接的两个字节由进程id组成(PID)

  • 最后三个字节是随机数。

MongoDB中存储的文档必须有一个”_id”键。这个键的值可以是任何类型的,默认是个ObjectId对象。
在一个集合里面,每个文档都有唯一的”_id”值,来确保集合里面每个文档都能被唯一标识。
MongoDB采用ObjectId,而不是其他比较常规的做法(比如自动增加的主键)的主要原因,因为在多个 服务器上同步自动增加主键值既费力还费时。

创建新的ObjectId

1
2
>newObjectId = ObjectId()
ObjectId("590b3cf564d6ed405ce2ad9d")

你也可以使用生成的id来取代MongoDB自动生成的ObjectId:

1
2
> myObjectId = ObjectId("590b3cf564d6ed405ce2ad9e")
ObjectId("590b3cf564d6ed405ce2ad9e")

创建文档的时间戳

由于 ObjectId 中存储了 4 个字节的时间戳,所以你不需要为你的文档保存时间戳字段,你可以通过 getTimestamp 函数来获取文档的创建时间:

1
2
> ObjectId("590b3cf564d6ed405ce2ad9e").getTimestamp()
ISODate("2017-05-04T14:38:45Z")

ObjectId 转换为字符串

在某些情况下,您可能需要将ObjectId转换为字符串格式:

1
2
> new ObjectId().str
590b3d5a64d6ed405ce2ad9e


MongoDB 自动增长

MongoDB 没有像 SQL 一样有自动增长的功能, MongoDB 的 _id 是系统自动生成的12字节唯一标识。
但在某些情况下,我们可能需要实现 ObjectId 自动增长功能。
由于 MongoDB 没有实现这个功能,我们可以通过编程的方式来实现,以下我们将在 counters 集合中实现_id字段自动增长。

举个栗子:

使用 counters 集合

首先创建 counters 集合:

1
2
> db.createCollection("counters")
{ "ok" : 1 }

然后我们向 counters 集合中插入以下文档,使用 productid 作为 key,sequence_value 字段是序列通过自动增长后的一个值:

1
2
> db.counters.insert({_id:"productid",sequence_value:0})
WriteResult({ "nInserted" : 1 })

MongoDB 原子操作

写函数之前我们首先来看一下MongoDB的原子操作

众所周知,Redis采用的是异步I/O非阻塞的单进程模型,每一条Redis命令都是原子性的,而MongoDB也为我们提供了一些原子操作

对单个文档进行原子性修改

mongoDB保证了对单个document的多个filed的原子性修改。如果需要对单个文档进行原子性的CAS操作(check and set),可以使用findAndModify操作。

比如下面就是一条原子性的CAS操作,首先选择_id为123的文档(注意这里只选择了一个文档),然后对计数器count加1,将status字段变为true,并返回修改后的结果。

1
2
3
4
5
6
db.colleciton.findAndModify({
... query:{_id:"123"},
... $inc:{count:1},
... $update:{status:true}
... },
... new:true)

对多个文档使用$isolate操作符

$isolate操作符可以对多个文档的修改提供隔离性。针对其他线程的并发写操作,$isolate保证了提交前其他线程无法修改对应的文档。针对其他线程的读操作,$isolate保证了其他线程读取不到未提交的数据。

但是$isolate有验证的性能问题,因为这种情况下线程持有锁的时间较长,严重的影响mongo的并发性。另外,$isolate也无法保证多个文档修改的一致性(all-or-nothing),$isolate失败是可能只修改了部分文档。

从语义层面实现事务性操作

mongoDB官方提供了一种做法,即两阶段提交(two-phase commit),基本的原理就是利用了写操作的幂等性。具体实现可以看官网的详细讲解。但是利用幂等性来实现事务性有一个重要的前置条件:业务不在乎中间态的不一致。幂等性可以保证最终的一致性,但是会出现中间的不一致状态。

原子操作栗子

先插入我们数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> db.books.insert(
... {
... "_id" : 123456789,
... "title" : "Journey Under the Midnight Sun",
... "author" :"Keigo Higashino",
... "published_date" : ISODate("2015-10-24T00:00:00Z"),
... "pages" : 539,
... "language" : "English",
... "publisher_id" : "Little, Brown",
... "available" : 20,
... "checkout" : [
... {
... "by" : "Little, Brown",
... "date" : ISODate("2015-08-15T00:00:00Z")
... }
... ]
... })
WriteResult({ "nInserted" : 1 })

你可以使用 db.collection.findAndModify() 方法来判断书籍是否可结算并更新新的结算信息

在同一个文档中嵌入的 available 和 checkout 字段来确保这些字段是同步更新的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
> db.books.findAndModify ( {
... query: {
... _id: 123456789,
... available: { $gt: 0 }
... },
... update: {
... $inc: { available: -1 },
... $push: { checkout: { by: "abc", date: new Date() } }
... }
... } )
{
"_id" : 123456789,
"title" : "Journey Under the Midnight Sun",
"author" : "Keigo Higashino",
"published_date" : ISODate("2015-10-24T00:00:00Z"),
"pages" : 539,
"language" : "English",
"publisher_id" : "Little, Brown",
"available" : 20,
"checkout" : [
{
"by" : "Little, Brown",
"date" : ISODate("2015-08-15T00:00:00Z")
}
]
}

原子操作常用命令

  • $set

用来指定一个键并更新键值,若键不存在并创建

1
{ $set : { field : value } }

  • $unset

用来删除一个键

1
{ $unset : { field : 1} }

  • $inc

$inc可以对文档的某个值为数字型(只能为满足要求的数字)的键进行增减的操作

1
{ $inc : { field : value } }

  • $push

把value追加到field里面去,field一定要是数组类型才行,如果field不存在,会新增一个数组类型加进去

1
{ $push : { field : value } }

  • $pushAll

同$push,只是一次可以追加多个值到一个数组字段内

1
{ $pushAll : { field : value_array } }

  • $pull

从数组field内删除一个等于value值。

1
{ $pull : { field : _value } }

  • $addToSet

增加一个值到数组内,而且只有当这个值不在数组内才增加。

  • $pop

删除数组的第一个或最后一个元素

1
{ $pop : { field : 1 } }

  • $rename

修改字段名称

1
{ $rename : { old_field_name : new_field_name } }

  • $bit

位操作,integer类型

1
{$bit : { field : {and : 5}}}

可以看到,大部分我们上一篇介绍到的操作都是原子操作

创建JavaScript函数

我们创建函数 getNextSequenceValue 来作为序列名的输入,指定的序列会自动增长 1 并返回最新序列值。在这里序列名为 productid ,并且findAndModify就是一个原子操作

1
2
3
4
5
6
7
8
9
>function getNextSequenceValue(sequenceName) {
var sequenceDocument = db.counters.findAndModify(
{
query:{_id: sequenceName},
update:{$inc:{sequence_value:1}},
new:true
});
return sequenceDocument.sequence_value;
}

使用JavaScript函数

1
2
3
4
5
6
7
8
9
10
> db.products.insert({
... "_id":getNextSequenceValue("productid"),
... "product_name":"MacBook",
... "category":"notebook"})
WriteResult({ "nInserted" : 1 })
> db.products.insert({
... "_id":getNextSequenceValue("productid"),
... "product_name":"iPhone8",
... "category":"mobiles"})
WriteResult({ "nInserted" : 1 })

可以发现,_id实现了自动增长

1
2
3
> db.products.find()
{ "_id" : 1, "product_name" : "MacBook", "category" : "notebook" }
{ "_id" : 2, "product_name" : "iPhone8", "category" : "mobiles" }

参考

深入分析mongoDB原子操作